package org.restorator.di;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import org.restorator.exception.CyclicReferencesException;
import org.restorator.exception.ServiceLocatorException;
import org.restorator.utill.GeneralUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.*;

import static com.google.common.base.Preconditions.*;

/**
 * @author Snisar Bogdan.
 */
class ServiceBuilder {

    private static Logger log = LoggerFactory.getLogger(GeneralUtil.getClassName());


    /**
     *  Container for all!
     */
    Map<String, Ref> container = new LinkedHashMap<>();


    public Map<String, Ref> getContainer() {
        return container;
    }

    /**
     * Add new ref instance to the container.
     * P.S. Before adding to this instance some injects, this inject ref must be append by
     * {@link ServiceBuilder#appendService(String, String, boolean)} this method.
     *
     * @param key ref key
     * @param clazz full class name
     * @param single true if singleton, false - prototype
     *
     * @throws ClassNotFoundException invalid class name
     */
    public void appendService(String key, String clazz, boolean single) throws ClassNotFoundException {
        if(!container.containsKey(key)) {
            container.put(key, newEmptyRef(key, clazz, single));
        }
    }

    /**
     *  Append some constructor to ref {@link Ref} that is in container. If no method or constructor
     *  will append to ref, {@link ServiceFactory} factory will use {@link Class#newInstance()} default
     *  constructor.
     *
     * @param key ref key
     * @param factory factory method name, or null if should used constructor
     * @param args array of args for constructor or factoryMethod
     * @throws NoSuchMethodException
     */
    public void appendConstructor(String key, String factory, Object[] args)
            throws NoSuchMethodException {
        checkState(container.containsKey(key), "Adding constructor to no existing ref");
        checkNotNull(args);
        log.trace("Adding not default constructor to {}", key);
            Class<?>[] classArgs = new Class[args.length];
            Ref ref = container.get(key);
            for (int i = 0; i < classArgs.length; i++) {
                if(args[i] instanceof Ref) {
                    Ref depended = (Ref) args[i];
                    classArgs[i] = depended.clazz;
                    addChildRef(ref, depended);
                } else {
                    classArgs[i] = args[i].getClass();
                }
            }

            if(factory == null) {
                ref.constructor = ref.clazz.getConstructor(classArgs);
                log.trace("Find constructor {}", ref.constructor);
                ref.constructorArgs = args;
                log.trace("Constructor arguments {}", Arrays.toString(args));
            } else {
                ref.factory = ref.clazz.getMethod(factory, classArgs);
//                if(ref.factory.getModifiers())
                log.trace("Find factory method {}", ref.factory);
                ref.factoryArgs = args;
                log.trace("Factory arguments {}", args);
            }
    }

    /**
     * Append methods to the ref that is in the container.
     * One arg array per one method name. If method with no arguments, this mast
     * be empty array (or null)
     *
     * @param key ref id
     * @param methodNames array of method names
     * @param args arrays of args for each argument array.
     * @throws NoSuchMethodException
     */
    public void appendMethods(String key, String[] methodNames, Object[][] args) throws NoSuchMethodException {
        checkState(container.containsKey(key), "Adding methods to no existing ref");
        if(methodNames != null) {
            Ref ref = container.get(key);
            Method[] methods = new Method[methodNames.length];

            for (int i = 0; i < methodNames.length; i++) {
                    Class<?>[] temp = new Class[args[i].length];
                    for (int j = 0; j < args[i].length; j++) {
                         if(args[i][j] instanceof Ref) {
                             Ref dependable = (Ref) args[i][j];
                             temp[j] = dependable.clazz;
                             addChildRef(ref, dependable);
                         } else {
                             temp[j] = args[i][j].getClass();
                         }
                    }
                log.trace("Ref'builder key <{}> getMethod by name {}, and class args {}", key, methodNames[i], temp);
                    methods[i] = ref.clazz.getMethod(methodNames[i], temp);
            }
            ref.methods = methods;
            ref.methodArgs = args;
        }
    }

    /**
     * Add
     *
     */
    private void addChildRef(Ref thisRef, Ref dependableRef) {
        checkCyclicState(thisRef, dependableRef);
        if(thisRef.childRef == null) {
            thisRef.childRef = new ArrayList<>(6);
        }
        thisRef.childRef.add(dependableRef);

        if(dependableRef.parentRef == null) {
            dependableRef.parentRef = new ArrayList<>(6);
        }
        dependableRef.parentRef.add(thisRef);
    }

    /**
     *
     * @param thisRef
     * @param dependedFromThis
     */
    private void checkCyclicState(Ref thisRef, Ref dependedFromThis) {
        if(thisRef.parentRef != null) {
            for(Ref ref : thisRef.parentRef) {
                checkCyclicState(ref, dependedFromThis);
            }
        } else {
            if(thisRef == dependedFromThis) {
                throw new CyclicReferencesException("Parent ref contained child ref as a super parent");
            }
        }
    }

    private Ref newEmptyRef(String key, String clazz, boolean single) throws ClassNotFoundException {
        if(key == null || clazz == null) {
            throw  new ServiceLocatorException("There is empty key {" + key + "} or class {" + clazz + "} attributes");
        }
        Ref ref = new Ref(key, Class.forName(clazz));
        ref.single = single;
        return ref;
    }

    /**
     *
     *
     * @param bar
     * @return
     */
    public Ref getRefFromKey(String bar) {
        return container.get(bar);
    }



    protected void clear() {

    }
}
